/*
 * Copyright 2015 Google, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.netflix.spinnaker.clouddriver.google.controllers;

import static com.netflix.spinnaker.clouddriver.google.cache.Keys.Namespace.IMAGES;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.compute.model.Image;
import com.google.common.annotations.VisibleForTesting;
import com.netflix.spinnaker.cats.cache.Cache;
import com.netflix.spinnaker.cats.cache.CacheData;
import com.netflix.spinnaker.cats.cache.RelationshipCacheFilter;
import com.netflix.spinnaker.cats.mem.InMemoryCache;
import com.netflix.spinnaker.clouddriver.google.GoogleCloudProvider;
import com.netflix.spinnaker.clouddriver.google.cache.Keys;
import groovy.util.logging.Slf4j;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/gce/images")
public class GoogleNamedImageLookupController {

  private final Cache cacheView;
  private final JacksonFactory jsonMapper = new JacksonFactory();
  private final ObjectMapper objectMapper = new ObjectMapper().configure(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS, true);

  @Autowired
  private GoogleNamedImageLookupController(Cache cacheView) {
    this.cacheView = cacheView;
  }

  @RequestMapping(value = "/find", method = RequestMethod.GET)
  public List<NamedImage> list(
    @RequestParam(required = false) String q,
    @RequestParam(required = false) String account,
    HttpServletRequest request
  ) {
    Map<String, List<Image>> imageMap = listImagesByAccount();

    // Set of accounts for which we should return images: either the supplied account, or default
    // to all accounts
    Set<String> accounts;
    if (StringUtils.isNotEmpty(account)) {
      accounts = new HashSet<>();
      if (imageMap.containsKey(account)) {
        accounts.add(account);
      }
    } else {
      accounts = imageMap.keySet();
    }

    List<NamedImage> results = new ArrayList<>();
    for (String imageAccount : accounts) {
      for (Image i : imageMap.get(imageAccount)) {
        Map<String, Object> attributes = new HashMap<>();
        attributes.put("creationDate", i.get("creationTimestamp"));
        NamedImage newImage = new NamedImage(imageAccount, i.getName(), attributes, buildTagsMap(i));
        results.add(newImage);
      }
    }

    Predicate<NamedImage> queryFilter = i -> true;
    if (q != null && q.trim().length() > 0) {
      String glob = q.trim();
      // Wrap in '*' if there are no glob-style characters in the query string.
      if (!glob.contains("*") && !glob.contains("?") && !glob.contains("[") && !glob.contains("\\")) {
        glob = "*" + glob + "*";
      }
      Pattern pattern = new InMemoryCache.Glob(glob).toPattern();
      queryFilter = i -> pattern.matcher(i.imageName).matches();
    }

    return results.stream()
      .filter(queryFilter)
      .filter(namedImage -> matchesTagFilters(namedImage, extractTagFilters(request)))
      .sorted(Comparator.comparing(image -> image.imageName))
      .collect(Collectors.toList());
  }

  private Map<String, List<Image>> listImagesByAccount() {
    Collection<String> identifiers = cacheView.filterIdentifiers(IMAGES.getNs(), GoogleCloudProvider.getID() + ":*");
    Map<String, List<Image>> result = new HashMap<>();

    Collection<CacheData> allCacheData = cacheView.getAll(IMAGES.getNs(), identifiers, RelationshipCacheFilter.none());
    allCacheData.forEach(
      cacheData -> {
        String account = Keys.parse(cacheData.getId()).get("account");
        if (!result.containsKey(account)) {
          result.put(account, new ArrayList<>());
        }
        Object hashImage = cacheData.getAttributes().get("image");
        try {
          Image myImage = jsonMapper.fromString(objectMapper.writeValueAsString(hashImage), Image.class);
          result.get(account).add(myImage);
        } catch (IOException e) {
          throw new RuntimeException("Image deserialization failed");
        }
      }
    );

    return result;
  }

  @VisibleForTesting
  public static Map<String, String> buildTagsMap(Image image) {
    Map<String, String> tags = new HashMap<>();

    String description = image.getDescription();
    // For a description of the form:
    // key1: value1, key2: value2, key3: value3
    // we'll build a map associating each key with
    // its associated value
    if (description != null) {
      tags = Arrays.stream(description.split(","))
        .filter(token -> token.contains(": "))
        .map(token -> token.split(": ", 2))
        .collect(Collectors.toMap(
          token -> token[0].trim(),
          token -> token[1].trim(),
          (a, b) -> b
        ));
    }

    Map<String, String> labels = image.getLabels();
    if (labels != null) {
      tags.putAll(labels);
    }

    return tags;
  }

  /**
   * Apply tag-based filtering to the list of named images.
   *
   * For example: /gce/images/find?q=PackageName&tag:stage=released&tag:somekey=someval
   */
  private static List<NamedImage> filter(List<NamedImage> namedImages, Map<String, String> tagFilters) {
    return namedImages.stream()
      .filter(namedImage -> matchesTagFilters(namedImage, tagFilters))
      .collect(Collectors.toList());
  }

  private static boolean matchesTagFilters(NamedImage namedImage, Map<String, String> tagFilters) {
    Map<String, String> tags = namedImage.tags;
    return tagFilters.keySet().stream()
      .allMatch(
        tag -> tags.containsKey(tag.toLowerCase()) && tags.get(tag.toLowerCase()).equalsIgnoreCase(tagFilters.get(tag))
      );
  }

  private static Map<String, String> extractTagFilters(HttpServletRequest httpServletRequest) {
    List<String> parameterNames = Collections.list(httpServletRequest.getParameterNames());

    return parameterNames.stream()
      .filter(Objects::nonNull)
      .filter(parameter -> parameter.toLowerCase().startsWith("tag:"))
      .collect(Collectors.toMap(
        tagParameter -> tagParameter.replaceAll("tag:", "").toLowerCase(),
        httpServletRequest::getParameter,
        (a, b) -> b
      ));
  }

  @VisibleForTesting
  public static class NamedImage {
    public String account;
    public String imageName;
    public Map<String, Object> attributes = new HashMap<>();
    public Map<String, String> tags = new HashMap<>();

    private NamedImage(String account, String imageName, Map<String, Object> attributes, Map<String, String> tags) {
      this.account = account;
      this.imageName = imageName;
      this.attributes = attributes;
      this.tags = tags;
    }
  }
}
